package com.codemonkey.lq.qd.extend.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;

/**
 * 实现Mybatis的xml映射文件的热部署
 * 
 * 通过扩展自HashMap的子类StripMap,
 *    覆写其put方法,将从被修改的xml映射的select/insert/update/delete对象在推入集合中前,先进行删除操作,无论其是否已存在.
 * 
 * @author LQ
 * @deprecated see {@code com.kanq.extend.mybatis.hotdeploy.MyBatisRefresh}
 */
@Deprecated
public class MyBatisRefresh implements Runnable {

	private final Configuration configuration;

	private List<FileBean> xmlFiles = new ArrayList<FileBean>();

	/**
	 *  记录进行了热备的mybatis文件
	 */
	private static final Map<Configuration, Boolean> configurationCache = new HashMap<Configuration, Boolean>();

	public static void startIt(Configuration configuration) {
		if (configurationCache.get(configuration) != null
				&& configurationCache.get(configuration).booleanValue()) {
			System.out.println("已经启动了: " + configuration.getClass().getName());
			return;
		}

		MyBatisRefresh myBatisRefresh = new MyBatisRefresh(configuration);
		new Thread(myBatisRefresh).start();
		configurationCache.put(configuration, true);
	}

	private MyBatisRefresh(Configuration configuration) {
		this.configuration = configuration;

		new FileScanner(configuration).getLoadedFile(xmlFiles);
		//getLoadedFile();
	}	

	@Override
	public void run() {
		try {
			refresh();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public synchronized void refresh() throws Exception {
		while (true) {
			// 获取需要刷新的Mapper文件列表
			List<FileBean> fileList = this.getModifiedFile();
			if (fileList != null) {

				System.out
						.println("########## there are some 'mybatis xml mapper files' changed.so we need refresh it ... ");

				clearSpecialFieldsInConfigurationClass();

				int size = fileList.size();
				for (int i = 0; i < size; i++) {
					FileBean fileBean = fileList.get(i);
					try {

						// 清理已加载的资源标识，方便让它重新加载。
						Field loadedResourcesField = this.configuration.getClass()
								.getDeclaredField("loadedResources");
						loadedResourcesField.setAccessible(true);
						Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
						String fileName = fileBean.getFile().getName();

						String removeFile = null;
						for (Iterator it = loadedResourcesSet.iterator(); it.hasNext();) {
							String resourceName = it.next() + "";
							if (resourceName.contains(fileName)) {
								removeFile = resourceName;
							}
						}
						loadedResourcesSet.remove(removeFile);

						//重新编译加载资源文件。
						InputStream inputSteam = new FileInputStream(fileBean.getFile());
						System.out.println(String.format(
								"  start refresh mybatis xml file [  %s ]", fileBean.getFile()
										.getAbsolutePath()));
						XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(inputSteam,
								this.configuration, fileBean.getFile().getAbsolutePath(),
								configuration.getSqlFragments());
						xmlMapperBuilder.parse();
						//标记文件上次修改时间
						fileBean.setBeforeTime();

					} catch (Exception e) {
						e.printStackTrace();
					} finally {
						//ErrorContext.instance().reset();  
					}
				}

				System.out.println("########## refresh finished  successfully ... ");
			}

			Thread.sleep(5 * 1000);
		}
	}

	private List<FileBean> getModifiedFile() throws IOException {
		List<FileBean> fileList = new ArrayList<FileBean>();
		for (FileBean fileBean : this.xmlFiles) {
			if (fileBean.isModify()) {
				fileList.add(fileBean);
			}
		}

		if (fileList != null && fileList.size() > 0) {
			return fileList;
		}
		return null;
	}

	/**
	 * 清理原有资源，更新为自己的StrictMap方便，增量重新加载
	 */
	private void clearSpecialFieldsInConfigurationClass() {
		String[] mapFieldNames = new String[] { "mappedStatements", "caches", "resultMaps",
				"parameterMaps", "keyGenerators", "sqlFragments" };
		for (String fieldName : mapFieldNames) {
			Field field = null;
			try {
				field = configuration.getClass().getDeclaredField(fieldName);
			} catch (NoSuchFieldException e) {
				e.printStackTrace();
			} catch (SecurityException e) {
				e.printStackTrace();
			}

			if (null == field) {
				continue;
			}

			field.setAccessible(true);

			Map map = null;
			try {
				map = ((Map) field.get(configuration));
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}

			if (null == map) {
				continue;
			}

			if (!(map instanceof StrictMap)) {
				Map newMap = new StrictMap(fieldName + "collection");
				for (Object key : map.keySet()) {
					try {
						newMap.put(key, map.get(key));
					} catch (IllegalArgumentException ex) {
						newMap.put(key, ex.getMessage());
					}
				}

				try {
					field.set(configuration, newMap);
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				}
			}
		}
	}

	public static class StrictMap<V> extends HashMap<String, V> {

		private static final long serialVersionUID = -4950446264854982944L;
		private String name;

		public StrictMap(String name, int initialCapacity, float loadFactor) {
			super(initialCapacity, loadFactor);
			this.name = name;
		}

		public StrictMap(String name, int initialCapacity) {
			super(initialCapacity);
			this.name = name;
		}

		public StrictMap(String name) {
			super();
			this.name = name;
		}

		public StrictMap(String name, Map<String, ? extends V> m) {
			super(m);
			this.name = name;
		}

		@SuppressWarnings("unchecked")
		public V put(String key, V value) {
			// ThinkGem 如果现在状态为刷新，则刷新(先删除后添加)
			if (true) {
				remove(key);
			}
			// ThinkGem end
			if (containsKey(key)) {
				throw new IllegalArgumentException(name + " already contains value for " + key);
			}
			if (key.contains(".")) {
				final String shortKey = getShortName(key);
				if (super.get(shortKey) == null) {
					super.put(shortKey, value);
				} else {
					super.put(shortKey, (V) new Ambiguity(shortKey));
				}
			}
			return super.put(key, value);
		}

		public V get(Object key) {
			V value = super.get(key);
			if (value == null) {
				throw new IllegalArgumentException(name + " does not contain value for " + key);
			}
			if (value instanceof Ambiguity) {
				throw new IllegalArgumentException(
						((Ambiguity) value).getSubject()
								+ " is ambiguous in "
								+ name
								+ " (try using the full name including the namespace, or rename one of the entries)");
			}
			return value;
		}

		private String getShortName(String key) {
			final String[] keyparts = key.split("\\.");
			return keyparts[keyparts.length - 1];
		}

		protected static class Ambiguity {
			private String subject;

			public Ambiguity(String subject) {
				this.subject = subject;
			}

			public String getSubject() {
				return subject;
			}
		}
	}

	/**
	 * 负责扫描出所有在热备时需要监视的文件系统文件
	 * @author LQ
	 *
	 */
	private static class FileScanner {

		private final Configuration config;

		public FileScanner(Configuration config) {
			super();
			this.config = config;
		}

		/**
		 * @param fileBeans 将要被填充的集合
		 */
		@SuppressWarnings("unchecked")
		public void getLoadedFile(final List<FileBean> fileBeans) {
			MetaObject metaSConfig = SystemMetaObject.forObject(config);
			Set<String> loadedResources = (Set<String>) metaSConfig.getValue("loadedResources");
			for (String loadedResource : loadedResources) {

				//TODO 这里有问题,Mybatis内部并不是直接存储路径
				// 需要一个严格的单元测试进行严密监视！！！！
				//果然需要一个严格的单元测试		
				File generatedFile = generateFile(loadedResource);
				if (null == generatedFile) {
					continue;
				}
				FileBean fileBean = new FileBean();
				fileBean.setFile(generatedFile);
				fileBean.setBeforeTime();
				fileBeans.add(fileBean);

			}
		}

		/**
		 * 按照MyBatis内部存储的规则,生成实际的加载文件; <br/>
		 * 
		 * 测试如下: <br/>
		 *		A. 使用org.mybatis.spring.SqlSessionFactoryBean来载入mybatis文件时,会有各种前缀: <br/>
		 *            1. file [D:\apache-tomcat-7.0.5\webapps\SpringMVCDemo\WEB-INF\classes\mapper\oracle\core.xml] <br/>
		 *            2. interface cn.springmvc.dao.UserDAO <br/>
		 *            3. namespace:cn.springmvc.dao.UserDAO <br/>   
		 *          原因: <br/>
		 *          	XMLMapperBuilder类中的bindMapperForNamespace()方法中的configuration.addLoadedResource("namespace:" + namespace); configuration.addMapper(boundType);导致了 <br/>              
		 *		B. 当使用Mybatis自身的配置文件导入时,没有该前缀;且为classpath <br/>
		 *		
		 *		以上仅仅针对 指定本地文件系统路径的情况
		 * @param loadedResource
		 * @return
		 */
		private File generateFile(String loadedResource) {
			if (!isResourcePath(loadedResource)) {
				return null;
			}

			final String location = preDealLocation(loadedResource);
			boolean isAbsolute = new File(location).isAbsolute();
			if (isAbsolute) {
				return new File(location);
			} else {
				// classpath load
				try {
					return Resources.getResourceAsFile(location);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

			return null;

		}

		/**
		 * 是否为合格的资源文件;因为使用org.mybatis.spring.SqlSessionFactoryBean来载入mybatis文件时,会有各种前缀file,namespace,interface.
		 * 由XMLMapperBuilder类中的bindMapperForNamespace()方法中的configuration.addLoadedResource("namespace:" + namespace); configuration.addMapper(boundType)导致
		 * 我们在这里假设为绝对路径的文件系统路径,或classpath下的路径
		 * @param loadedResource
		 * @return
		 */
		private boolean isResourcePath(String loadedResource) {
			//不以namespace和interface开头
			return (!(loadedResource.startsWith("namespace") || loadedResource
					.startsWith("interface")));
		}

		private String preDealLocation(String loadedResource) {
			if (loadedResource.startsWith("file")) {
				int startIndex = loadedResource.indexOf("[");
				int endIndex = loadedResource.indexOf("]");
				return loadedResource.substring(startIndex + 1, endIndex);
			}

			return loadedResource;
		}
	}
}
